package com.ybb.vue.common.webSocket;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ybb.vue.common.constant.JwtConstant;
import com.ybb.vue.common.util.JwtUtils;
import com.ybb.vue.entity.CheckResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author ybb
 * @version 1.0
 * @Description WebSocket
 * @Date 2023/3/31 13:46
 */

@Component
//这个注解可以理解为@RequestMapping，后面是接口地址和参数
@ServerEndpoint(value = "/webSocket/{token}",encoders = { ServerEncoderAnn.class ,ServerEncoderAlarm.class})
@Slf4j
public class WebSocket {
    //定义的存储类，用于保存对应的用户名，向对应的用户推送消息
    private static final Map<String, Session> TOKEN_SESSION = new HashMap<>();
    private static final Map<String, String> SESSION_ID_TOKEN = new HashMap<>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(@PathParam("token") String token, Session session) {
        String username = JwtUtils.parseJWT(token).getSubject();
        if (username == null) {
            try {
                //检测路径参数为空时断开链接。session.close是断开链接的意思。
                session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "username参数为空"));
                return;
            } catch (IOException e) {
                throw new IllegalStateException(e.getMessage());
            }
        }
        // 验证token
        CheckResult checkResult = JwtUtils.validateJWT(token);
        if (!checkResult.isSuccess()) {
            if (checkResult.getErrCode() == JwtConstant.JWT_ERRCODE_NULL) {
                try {
                    session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "token不存在"));
                    return;
                } catch (IOException e) {
                    throw new IllegalStateException(e.getMessage());
                }
            }
            if (checkResult.getErrCode() == JwtConstant.JWT_ERRCODE_EXPIRE) {
                try {
                    session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "token已过期"));
                    return;
                } catch (IOException e) {
                    throw new IllegalStateException(e.getMessage());
                }
            }
            if (checkResult.getErrCode() == JwtConstant.JWT_ERRCODE_FAIL) {
                try {
                    session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "token验证失败"));
                    return;
                } catch (IOException e) {
                    throw new IllegalStateException(e.getMessage());
                }
            }
        }
        //往存储类中添加用户的信息。
        SESSION_ID_TOKEN.put(session.getId(), username);
        TOKEN_SESSION.put(username, session);
        log.info("有新的连接,name为{}，总数{}", username, SESSION_ID_TOKEN.size());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        //关闭连接
        TOKEN_SESSION.remove(SESSION_ID_TOKEN.get(session.getId()));
        log.info("有连接断开,name为{}，剩余总数{}", SESSION_ID_TOKEN.get(session.getId()), TOKEN_SESSION.size());
        SESSION_ID_TOKEN.remove(session.getId());
        session.close();
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        if (StringUtils.isNotBlank(message)) {
            log.info("------进入心跳检测------");
            try {//解析发送的报文
                // 如果是心跳检测的消息，则原路返回pong作为心跳回应
                if (message.equalsIgnoreCase("ping")) {
                    Map<String, Object> params = new HashMap<>();
                    params.put("type", "pong");
                    sendMessageToTarget(SESSION_ID_TOKEN.get(session.getId()), JSON.toJSONString(params));
                    System.out.println("应答客户端的消息:" + JSON.toJSONString(params));
                } else {
                    sendMessage("接收到客户端消息" + message);
                }
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("Session:" + session + ",错误原因:" + error.getMessage());
        error.printStackTrace();
    }

    //群发信息的方法
    public void sendMessage(Object object) {
        log.info("发送全体消息");
        //循环全部人员信息
        TOKEN_SESSION.values().forEach((session) -> {
            //向每个用户发送文本信息。这里getAsyncRemote（）解释一下，向用户发送文本信息有两种方式，一种是getBasicRemote，一种是getAsyncRemote
            //区别：getAsyncRemote是异步的，不会阻塞，而getBasicRemote是同步的，会阻塞，由于同步特性，第二行的消息必须等待第一行的发送完成才能进行。
            // 而第一行的剩余部分消息要等第二行发送完才能继续发送，所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
            // 则避免尽量一次发送全部消息，使用部分消息来发送，可以看到下面sendMessageToTarget方法内就用的getBasicRemote，因为这个方法是根据用户id来私发的，所以不是全部一起发送。
            session.getAsyncRemote().sendObject(object);
        });
    }

    //私发信息的方法（根据用户id把信息只发送给对应的人）
    //参数为：token是用户的id。后面t是发送的信息
    public <T> void sendMessageToTarget(String token, String t) {
        log.info("发送指定token消息");
        try {
            //发送信息，从TOKEN_SESSION存储类中找到对应id的用户，用getBasicRemote().sendText发送信息
            if (TOKEN_SESSION.get(token) != null) {
                TOKEN_SESSION.get(token).getBasicRemote().sendText(t);
            }
        } catch (Exception e) {
            //如果报错就抛出异常
            e.printStackTrace();
        }
    }

    //私发信息的方法（根据用户id把信息只发送给对应的人）
    //参数为：token是用户的id。后面t是发送的信息 object类型
    public <T> void sendMessageToTargetObject(String token, Object t) {
        log.info("发送指定token消息");
        try {
            //发送信息，从TOKEN_SESSION存储类中找到对应id的用户，用getBasicRemote().sendText发送信息
            if (TOKEN_SESSION.get(token) != null) {
                TOKEN_SESSION.get(token).getBasicRemote().sendObject(t);
            }
        } catch (Exception e) {
            //如果报错就抛出异常
            e.printStackTrace();
        }
    }
}
